﻿// -----------------------------------------------------------------------
// <copyright file="ScopeStack.cs" company="Ubiquity.NET Contributors">
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Kaleidoscope.Grammar
{
    /// <summary>Simple implementation of common Variable scoping</summary>
    /// <typeparam name="T">Type of the values to associate with the symbol name</typeparam>
    /// <remarks>
    /// <para>In essence, this is a stack of Dictionaries that is intended for use in code generation.
    /// Most languages have some sort of notion of symbol scopes and name lookups. This implements
    /// the common case of nested scopes where a new 'local scope' may override some of the symbols
    /// in a parent scope. Any values in any parent not overridden by the child are visible to the
    /// child scope.</para>
    /// </remarks>
    internal class ScopeStack<T>
    {
        /// <summary>Initializes a new instance of the <see cref="ScopeStack{T}"/> class.</summary>
        /// <remarks>
        /// The stack is initialized with a "global" scope ready for use without additional
        /// initialization. Subsequent scopes for the stack are generated by calling the
        /// <see cref="EnterScope"/> method.
        /// </remarks>
        public ScopeStack( )
        {
            Scopes.Push( new Dictionary<string, T>( ) );
        }

        /// <summary>Starts a new scope</summary>
        /// <returns><see cref="IDisposable"/> to enable automatic restore of the scope in RAII style patterns</returns>
        /// <remarks>
        /// A new scope is pushed on the stack and remains active until the <see cref="IDisposable.Dispose"/> method
        /// of the return is called. Normally, code generation does this with a language provided means of ensuring the
        /// Dispose method is called even when an exception occurs. (i.e. C# 'using' or try/finally)
        /// </remarks>
        public IDisposable EnterScope( )
        {
            Scopes.Push( new Dictionary<string, T>( ) );
            return new DisposableAction( ( ) => Scopes.Pop( ) );
        }

        /// <summary>Gets or sets the value of a symbol in scope</summary>
        /// <param name="name">name of the symbol</param>
        /// <returns>Value for the symbol</returns>
        /// <remarks>
        /// Getting a symbol value searches all scopes, starting with the current scope.
        /// Setting a symbol value will only set the value in the current scope. (e.g.
        /// if the value does not exist in the current scope a new entry is made for it
        /// in the current scope, even if some outer scope has the same name.
        /// </remarks>
        /// <exception cref="KeyNotFoundException">If the <paramref name="name"/> wasn't found in the active or parent scopes</exception>
        public T this[ string name ]
        {
            get => TryGetValue( name, out T retVal ) ? retVal : throw new KeyNotFoundException( name );
            set => Scopes.Peek( )[ name ] = value;
        }

        /// <summary>Attempts to retrieve a value from the stack</summary>
        /// <param name="name">Name of the value</param>
        /// <param name="value">Value</param>
        /// <returns><see langword="true"/> if the symbol was found from a search of the scopes</returns>
        public bool TryGetValue( string name, [MaybeNullWhen( false )] out T value )
        {
            foreach( var scope in Scopes )
            {
                if( scope.TryGetValue( name, out value ) )
                {
                    return true;
                }
            }

            value = default!;
            return false;
        }

        private readonly Stack<IDictionary<string, T>> Scopes = new Stack<IDictionary<string, T>>();
    }
}
